package org.dawnmew.jpurewave;

/**
 * Parses and plays back an OGG-encoded sound file on the dialog output line.
 *
 * @author dawnmew
 * @since 1.0
 *
 * Licensed under the MIT License.
 *
 * Copyright (c) 2012 dawnmew
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
import com.jcraft.jogg.Packet;
import com.jcraft.jogg.Page;
import com.jcraft.jogg.StreamState;
import com.jcraft.jogg.SyncState;
import com.jcraft.jorbis.Block;
import com.jcraft.jorbis.Comment;
import com.jcraft.jorbis.DspState;
import com.jcraft.jorbis.Info;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.dawnmew.jvirtualstream.ResettableInputStream;
import org.dawnmew.jvirtualstream.VirtualFileSystem;

public class DialogInstance extends Thread
{

    private ResettableInputStream inputStream = null;
    byte[] buffer = null;
    private static final int bufferSize = 2048;
    int count = 0;
    int index = 0;
    byte[] convertedBuffer = null;
    int convertedBufferSize = 0;
    private float[][][] pcmInfo = null;
    private int[] pcmIndex = null;
    private Packet joggPacket = null;
    private Page joggPage = null;
    private StreamState joggStreamState = null;
    private SyncState joggSyncState = null;
    private DspState jorbisDspState = null;
    private Block jorbisBlock = null;
    private Comment jorbisComment = null;
    private Info jorbisInfo = null;
    private boolean ready = false;
    private boolean paused = false;
    private boolean dying = false;
    private boolean dead = false;
    private static final Object readyLock = new Object();
    private static final Object deathLock = new Object();
    private static final Object deadLock = new Object();
    private static final Object pauseLock = new Object();
    private SoundSystem parent = null;

    DialogInstance(String inputFile, SoundSystem parent)
    {
        this.inputStream = new ResettableInputStream(inputFile, VirtualFileSystem.getInstance());
        this.parent = parent;
    }

    @Override
    public void run()
    {
        if (!inputStream.isValid())
        {
            System.err.println("We don't have an input stream and therefore "
                    + "cannot continue.");
            return;
        }
        count = 0;
        index = 0;
        convertedBuffer = null;
        convertedBufferSize = 0;
        pcmInfo = null;

        pcmIndex = null;

        joggPacket = new Packet();
        joggPage = new Page();
        joggStreamState = new StreamState();
        joggSyncState = new SyncState();

        jorbisDspState = new DspState();
        jorbisBlock = new Block(jorbisDspState);
        jorbisComment = new Comment();
        jorbisInfo = new Info();
        joggSyncState.init();
        joggSyncState.buffer(bufferSize);
        buffer = joggSyncState.data;
        if (readHead())
        {
            convertedBufferSize = bufferSize * 2;
            convertedBuffer = new byte[convertedBufferSize];
            jorbisDspState.synthesis_init(jorbisInfo);
            jorbisBlock.init(jorbisDspState);
            pcmInfo = new float[1][][];
            pcmIndex = new int[jorbisInfo.channels];
            readBody();
            synchronized (deathLock)
            {
                dying = true;
                deathLock.notifyAll();
            }
            joggStreamState.clear();
            jorbisBlock.clear();
            jorbisDspState.clear();
            jorbisInfo.clear();
            joggSyncState.clear();
            inputStream.restart();
        }
        synchronized (deadLock)
        {
            synchronized (readyLock)
            {
                ready = false;
                readyLock.notifyAll();
            }
            synchronized (pauseLock)
            {
                paused = true;
                pauseLock.notifyAll();
            }
            synchronized (deathLock)
            {
                dying = false;
                deathLock.notifyAll();
            }
            dead = true;
            deadLock.notifyAll();
        }
    }

    private boolean readHead()
    {
        boolean needMoreData = true;
        int packet = 1;
        while (needMoreData)
        {
            try
            {
                count = inputStream.read(buffer, index, bufferSize);
            } catch (IOException exception)
            {
                System.err.println("Could not read from the input stream.");
                System.err.println(exception);
            }

            joggSyncState.wrote(count);

            switch (packet)
            {
                case 1:
                {
                    switch (joggSyncState.pageout(joggPage))
                    {
                        case -1:
                        {
                            System.err.println("There is a hole in the first "
                                    + "packet data.");
                            return false;
                        }

                        case 0:
                        {
                            break;
                        }

                        case 1:
                        {
                            joggStreamState.init(joggPage.serialno());
                            joggStreamState.reset();

                            jorbisInfo.init();
                            jorbisComment.init();

                            if (joggStreamState.pagein(joggPage) == -1)
                            {
                                System.err.println("We got an error while "
                                        + "reading the first header page.");
                                return false;
                            }

                            if (joggStreamState.packetout(joggPacket) != 1)
                            {
                                System.err.println("We got an error while "
                                        + "reading the first header packet.");
                                return false;
                            }

                            if (jorbisInfo.synthesis_headerin(jorbisComment,
                                    joggPacket) < 0)
                            {
                                System.err.println("We got an error while "
                                        + "interpreting the first packet. "
                                        + "Apparantly, it's not Vorbis data.");
                                return false;
                            }

                            packet++;
                            break;
                        }
                    }

                    if (packet == 1)
                    {
                        break;
                    }
                }

                case 2:
                case 3:
                {
                    switch (joggSyncState.pageout(joggPage))
                    {
                        case -1:
                        {
                            System.err.println("There is a hole in the second "
                                    + "or third packet data.");
                            return false;
                        }

                        case 0:
                        {
                            break;
                        }
                        case 1:
                        {
                            joggStreamState.pagein(joggPage);

                            switch (joggStreamState.packetout(joggPacket))
                            {
                                case -1:
                                {
                                    System.err.println("There is a hole in the first"
                                            + "packet data.");
                                    return false;
                                }

                                case 0:
                                {
                                    break;
                                }

                                case 1:
                                {
                                    jorbisInfo.synthesis_headerin(
                                            jorbisComment, joggPacket);

                                    packet++;

                                    if (packet == 4)
                                    {
                                        needMoreData = false;
                                    }

                                    break;
                                }
                            }

                            break;
                        }
                    }

                    break;
                }
            }

            index = joggSyncState.buffer(bufferSize);
            buffer = joggSyncState.data;

            if (count == 0 && needMoreData)
            {
                System.err.println("Not enough header data was supplied.");
                return false;
            }
        }
        return true;
    }

    private void readBody()
    {
        boolean needMoreData = true;

        while (needMoreData)
        {
            switch (joggSyncState.pageout(joggPage))
            {
                case 0:
                {
                    break;
                }

                case 1:
                {
                    joggStreamState.pagein(joggPage);

                    if (joggPage.granulepos() == 0)
                    {
                        needMoreData = false;
                        break;
                    }

                    processPackets:
                    while (true)
                    {
                        switch (joggStreamState.packetout(joggPacket))
                        {
                            case 0:
                            {
                                break processPackets;
                            }

                            case 1:
                            {
                                decodeCurrentPacket();
                            }
                        }
                    }

                    synchronized (deathLock)
                    {
                        if (joggPage.eos() != 0 || dying)
                        {
                            needMoreData = false;
                        }
                    }
                }
            }

            if (needMoreData)
            {
                index = joggSyncState.buffer(bufferSize);
                buffer = joggSyncState.data;

                try
                {
                    count = inputStream.read(buffer, index, bufferSize);
                } catch (Exception e)
                {
                    return;
                }

                if (count >= 0)
                {
                    joggSyncState.wrote(count);
                }

                if (count <= 0)
                {
                    needMoreData = false;
                }
            }
        }
    }

    private void decodeCurrentPacket()
    {
        int samples;

        if (jorbisBlock.synthesis(joggPacket) == 0)
        {
            jorbisDspState.synthesis_blockin(jorbisBlock);
        }

        int range;

        while ((samples = jorbisDspState.synthesis_pcmout(pcmInfo, pcmIndex))
                > 0)
        {
            if (samples < convertedBufferSize)
            {
                range = samples;
            } else
            {
                range = convertedBufferSize;
            }

            for (int i = 0; i < jorbisInfo.channels; i++)
            {
                int sampleIndex = i * 2;

                for (int j = 0; j < range; j++)
                {
                    int value = (int) (pcmInfo[0][i][pcmIndex[i] + j] * 32767);

                    if (value > 32767)
                    {
                        value = 32767;
                    }
                    if (value < -32768)
                    {
                        value = -32768;
                    }

                    if (value < 0)
                    {
                        value = value | 32768;
                    }

                    convertedBuffer[sampleIndex] = (byte) (value);
                    convertedBuffer[sampleIndex + 1] = (byte) (value >>> 8);

                    sampleIndex += 2 * (jorbisInfo.channels);
                }
            }

            jorbisDspState.synthesis_read(range);

            synchronized (readyLock)
            {
                if (!ready)
                {
                    ready = true;
                    readyLock.notifyAll();
                }
            }

            synchronized (pauseLock)
            {
                while (paused)
                {
                    try
                    {
                        pauseLock.wait();
                    } catch (InterruptedException ex)
                    {
                        Logger.getLogger(DialogInstance.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
            synchronized (deathLock)
            {
                if (dying)
                {
                    return;
                }
            }
            synchronized (parent.dialogLineSync)
            {
                parent.dialogLine.write(convertedBuffer, 0, 2 * jorbisInfo.channels
                        * range);
            }
        }
    }

    public void pause()
    {
        waitUntilReady();
        synchronized (pauseLock)
        {
            paused = true;
            pauseLock.notifyAll();
        }
    }

    public void end()
    {
        synchronized (deathLock)
        {
            dying = true;
            deathLock.notifyAll();
        }
        synchronized (pauseLock)
        {
            paused = false;
            pauseLock.notifyAll();
        }
    }

    public void waitUntilDone()
    {
        synchronized (deadLock)
        {
            while (!dead)
            {
                try
                {
                    deadLock.wait();
                } catch (InterruptedException ex)
                {
                    Logger.getLogger(DialogInstance.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }

    public void waitUntilReady()
    {
        synchronized (readyLock)
        {
            while (!ready)
            {
                try
                {
                    readyLock.wait();
                } catch (InterruptedException ex)
                {
                    Logger.getLogger(DialogInstance.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }
}
